import {Injectable} from '@angular/core';
import {Hero} from "./Hero";
import {HEROES} from "./mock-heroes";
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {catchError, map, tap} from 'rxjs/operators';
// 可观察（Observable）的数据
import {Observable, of} from "rxjs";

import {MessageService} from "./message.service";
// 注意，这个新的服务导入了 Angular 的 Injectable 符号
// 并且给这个服务类添加了 @Injectable() 装饰器。
// 它把这个类标记为依赖注入系统的参与者之一
// @Injectable() 装饰器会接受该服务的元数据对象，就像 @Component() 对组件类的作用一样
// 你必须先注册一个服务提供者，来让 HeroService 在依赖注入系统中可用，Angular 才能把它注入组件
// 所谓服务提供者就是某种可用来创建或交付一个服务的东西；在这里，它通过实例化 HeroService 类，来提供该服务。

@Injectable({
  providedIn: 'root'
}) //用根注入器将你的服务注册成为提供者。
/**
 * HeroService 必须等服务器给出响应，
 * 而 getHeroes() 不能立即返回英雄数据，
 * 浏览器也不会在该服务等待期间停止响应
 *
 * HeroService.getHeroes() 必须具有某种形式的异步函数签名
 */
export class HeroService {
  // 把服务器上英雄数据资源的访问地址 heroesURL 定义为 :base/:collectionName 的形式
  // 这里的 base 是要请求的资源
  // 而 collectionName 是 in-memory-data-service.ts 中的英雄数据对象
  private heroesUrl = 'api/heroes';  // URL to web api

  // 服务中的服务
  constructor(private messageService: MessageService,
              private http: HttpClient) {
  }

  /**
   * web API 期待在保存时的请求中有一个特殊的头
   * 这个头是在 HeroService 的 httpOptions 常量中定义的。
   */
  httpOptions = {
    headers: new HttpHeaders({'Content-Type': 'application/json'})
  };

  /**
   * 处理失败的Http操作。
   * Let the app continue.
   *
   * @param operation -失败的操作的名称
   * @param result -作为可观察结果返回的可选值
   */
  /**
   * 返回一个 Observable<Hero[]>
   *
   * 可观察对象版本的 HeroService
   *
   * of(HEROES) 会返回一个 Observable<Hero[]>，它会发出单个值，这个值就是这些模拟英雄的数组。
   *
   * 调用 HttpClient.get<Hero[]>() 它也同样返回一个 Observable<Hero[]>，它也会发出单个值，这个值就是来自 HTTP 响应体中的英雄数组
   */
  // getHeroes(): Observable<Hero[]> {
  //   const heroes = of(HEROES)
  //   return heroes
  // }
  /**
   * GET heroes from the server
   * HttpClient 的方法返回单个值
   * 所有的 HttpClient 方法都会返回某个值的 RxJS Observable。
   *
   * HTTP 是一个请求/响应式协议。你发起请求，它返回单个的响应。
   *
   * 通常，Observable 可以在一段时间内返回多个值。但来自 HttpClient 的 Observable 总是发出一个值，然后结束，再也不会发出其它值。
   *
   * 具体到这次 HttpClient.get() 调用，它返回一个 Observable<Hero[]>，也就是“一个英雄数组的可观察对象”。在实践中，它也只会返回一个英雄数组。
   *
   * HttpClient.get() 返回响应数据
   * HttpClient.get() 默认情况下把响应体当做无类型的 JSON 对象进行返回。如果指定了可选的模板类型 <Hero[]>，就会给返回你一个类型化的对象。
   *
   * 服务器的数据 API 决定了 JSON 数据的具体形态。英雄之旅的数据 API 会把英雄数据作为一个数组进行返回。
   *
   * HeroService 的方法将会窥探 Observable 的数据流，并通过 log() 方法往页面底部发送一条消息。
   *
   * 要捕获错误，你就要使用 RxJS 的 catchError() 操作符来建立对 Observable 结果的处理管道（pipe）
   *
   *
   * 它们可以使用 RxJS 的 tap() 操作符来实现
   * 该操作符会查看 Observable 中的值，使用那些值做一些事情，并且把它们传出来
   * 这种 tap() 回调不会改变这些值本身。
   */
  getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl)
      .pipe(
        tap(_ => this.log('fetched heroes')),
        catchError(this.handleError<Hero[]>('getHeroes', []))
      );
  }


  /**
   * 用 RxJS 的 of() 函数返回一个 Observable 形式的模拟英雄数据。
   * @param id
   * 以 :baseURL/:id 的形式根据 id 进行获取
   *
   * getHero() 使用想获取的英雄的 id 构造了一个请求 URL
   *
   * 服务器应该使用单个英雄作为回应，而不是一个英雄数组
   *
   * 所以，getHero() 会返回 Observable<Hero>（“一个可观察的单个英雄对象”）
   * 而不是一个可观察的英雄对象数组
   */
  getHero(id: number): Observable<Hero> {
    // const hero = HEROES.find(h => h.id === id)!;
    // // 反引号 ( ` ) 用于定义 JavaScript 的 模板字符串字面量，以便嵌入 id。
    // this.messageService.add(`HeroService: fetched hero id=${id}`);
    // return of(hero);


    /** GET hero by id. Will 404 if id not found */
    const url = `${this.heroesUrl}/${id}`;
    console.log(url)
    return this.http.get<Hero>(url).pipe(
      tap(_ => this.log(`fetched hero id=${id}`)),
      catchError(this.handleError<Hero>(`getHero id=${id}`))
    );
  }

  /** 使用MessageService记录HeroService消息 */
  private log(message: string) {
    this.messageService.add(`HeroService: ${message}`);
  }

  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {

      // TODO: 将错误发送到远程日志记录基础结构
      console.error(error); // log to console instead

      // TODO: 更好地转换用户消费的错误
      this.log(`${operation} failed: ${error.message}`);

      //返回一个空结果，让应用程序继续运行。
      return of(result as T);
    };
  }

  /** PUT: update the hero on the server
   * 使用 http.put() 来把修改后的英雄保存到服务器上。把下列代码添加进 HeroService。
   * HttpClient.put() 方法接受三个参数：
   *
   * URL 地址
   *
   * 要修改的数据（这里就是修改后的英雄）
   *
   * 选项
   */
  updateHero(hero: Hero): Observable<any> {
    console.log(this.heroesUrl)
    console.log(hero)
    return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
      tap(_ => this.log(`updated hero id=${hero.id}`)),
      catchError(this.handleError<any>('updateHero'))
    );
  }

  /** POST: add a new hero to the server */
  addHero(hero: Hero): Observable<Hero> {
    console.log(this.heroesUrl)
    return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
      tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
      catchError(this.handleError<Hero>('addHero'))
    );
  }

  /** DELETE: delete the hero from the server
   *
   * deleteHero() 调用了 HttpClient.delete()
   *
   * URL 就是英雄的资源 URL 加上要删除的英雄的 id
   *
   * 用像 put() 和 post() 中那样发送任何数据
   *
   * 仍要发送 httpOptions
   */
  deleteHero(id: number): Observable<Hero> {
    const url = `${this.heroesUrl}/${id}`;
    console.log(this.heroesUrl)
    return this.http.delete<Hero>(url, this.httpOptions).pipe(
      tap(_ => this.log(`deleted hero id=${id}`)),
      catchError(this.handleError<Hero>('deleteHero'))
    );
  }

  /* GET heroes whose name contains search term
  *
  * 如果没有搜索词，该方法立即返回一个空数组
  * 剩下的部分和 getHeroes() 很像
  * 唯一的不同点是 URL，它包含了一个由搜索词组成的查询字符串
  *
  */
  searchHeroes(term: string): Observable<Hero[]> {
    if (!term.trim()) {
      // if not search term, return empty hero array.
      return of([]);
    }
    return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
      tap(x => x.length ?
        this.log(`found heroes matching "${term}"`) :
        this.log(`no heroes matching "${term}"`)),
      catchError(this.handleError<Hero[]>('searchHeroes', []))
    );
  }

  // 返回 Hero[]数组
  // getHeroes(): Hero[] {
  //   return HEROES;
  // }
}
